<?php
/**
 * Zend_Jabber is a connection wraper class. 
 * 
 * This class is responsibilе for connecting to server, sending and receiving xmpp stanzas
 */
class Zend_Jabber_Connection {
    /**
     * Connection port
     * 
     * @var integer
     */
    private $port = 5222;
    
    /**
     * Connection socket
     * 
     * @var resource
     */
    private $socket = null;
   
    
    /**
     * Socket stream error number 
     * 
     * @var integer
     */
    private $socketErrorNumber = 0;
    
    /**
     * Socket stream error message
     * 
     * @var string
     */
    private $socketErrorString = '';
    
    /**
     * Response timeout in seconds
     * 
     * When the time has expiered receive method returns a null response
     * 
     * @var integer
     */
    private $socketTimeout = 0.1;
    
    /**
     * Specifies how much bytes per read we should scan
     * 
     * @var integer
     */
    private $socketReadBytes = 16384;
    
    /**
     * Connection timeout
     * 
     * @var integer
     */
    private $socketConnectionTimeout = 10;
        
    /**
     * Zend_Jabber server host name
     * 
     * @var string
     */
    private $host;
    
    /**
     * JID domain name. 
     * 
     * Most of the time it is the same as server hostname, but sometimes may differ 
     * 
     * @var string
     */
    private $domain;
    
    /**
     * Client's stream Dom
     * 
     * @var DomDocument
     */
    private $clientStream;

    /**
     * Server's stream dom
     * 
     * @var DomDocument
     */
    private $serverStream;
    
    /**
     * Array of server features. 
     * 
     * The contents of this array contain features of last opened stream only
     * 
     * @var array
     */
    private $features = array();
    
    /**
     * Available authentication mechanisms. 
     * 
     * @var array
     */
    private $mechanisms = array();
    
    /**
     * Opening client stream tag template
     * 
     * @var string
     */
    private $clientStreamStart = "<stream:stream to='%s' xmlns='%s' xmlns:stream='%s' version='%s'>";
    
    /**
     * Opening server stream tag template
     * 
     * @var string
     */
    private $serverStreamStart = '<stream:stream xmlns="%s" xmlns:stream="%s" from="%s" version="%s">';

    /**
     * Supported XMPP protocol version
     */
    const PROTOCOL_VERSION = '1.0';

    /**
     * Run in debugind mode if true
     */
    const DEBUG = true;
    
    // Namespaces
    const NS_CLIENT = 'jabber:client';
    const NS_SERVER = 'jabber:server';
    const NS_AUTH = 'jabber:iq:auth';
    const NS_REGISTER = 'jabber:iq:register';
    const NS_ROSTER = 'jabber:iq:roster';
    const NS_OFFLINE = 'jabber:x:offline';
    const NS_AGENT = 'jabber:iq:agent';
    const NS_AGENTS = 'jabber:iq:agents';
    const NS_DELAY = 'jabber:x:delay';
    const NS_VERSION = 'jabber:iq:version';
    const NS_TIME = 'jabber:iq:time';
    const NS_VCARD = 'vcard-temp';
    const NS_PRIVATE = 'jabber:iq:private';
    const NS_SEARCH = 'jabber:iq:search';
    const NS_OOB = 'jabber:iq:oob';
    const NS_XOOB = 'jabber:x:oob';
    const NS_ADMIN = 'jabber:iq:admin';
    const NS_FILTER = 'jabber:iq:filter';
    const NS_AUTH_0K = 'jabber:iq:auth:0k';
    const NS_BROWSE = 'jabber:iq:browse';
    const NS_EVENT = 'jabber:x:event';
    const NS_CONFERENCE = 'jabber:iq:conference';
    const NS_SIGNED = 'jabber:x:signed';
    const NS_ENCRYPTED = 'jabber:x:encrypted';
    const NS_GATEWAY = 'jabber:iq:gateway';
    const NS_LAST = 'jabber:iq:last';
    const NS_ENVELOPE = 'jabber:x:envelope';
    const NS_EXPIRE = 'jabber:x:expire';
    const NS_XHTML = 'http://www.w3.org/1999/xhtml';
    const NS_XDBGINSERT = 'jabber:xdb:ginsert';
    const NS_XDBNSLIST = 'jabber:xdb:nslist';
    const NS_TLS = 'urn:ietf:params:xml:ns:xmpp-tls';
    const NS_STREAM = 'http://etherx.jabber.org/streams';
    const NS_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl';
    const NS_BIND = 'urn:ietf:params:xml:ns:xmpp-bind';
    const NS_SESSION = 'urn:ietf:params:xml:ns:xmpp-session';
    
    // Mechanisms
    const MECHANISM_PLAIN = 'PLAIN';
    const MECHANISM_DIGEST_MD5 = 'DIGEST-MD5';

    
    /**
     * Constructor. Creates new connection wrapper object, but does not connects to the server
     * 
     * @param string $host Server host name
     * @param integer $port Server port number
     * @param string $domain JID domain name. Most of the time it is the same as server hostname, but sometimes may differ
     * @see self::connect()
     * @see self::disconnect()
     */
    public function __construct($host, $port = 5222, $domain = false) {
        $this->host = $host;
        $this->port = $port;
        $this->domain = $domain ? $domain : $this->host;
    }
    
    /**
     * Get array of used authentication mechanisms
     * 
     * @return array
     */
    public function getMechanisms() {
        return $this->mechanisms;
    }
    
    /**
     * Get array of available stream features
     * 
     * @return array
     */
    public function getFeatures() {
        return $this->features;
    }
    
    /**
     * Get server host name
     * 
     * @return string
     */
    public function getHost() {
        return $this->host;
    }
    
    /**
     * Get JID domain name
     * 
     * @return string
     */
    public function getDomain() {
        return $this->domain;
    }
    
    /**
     * Open connection with Jabber server.
     * 
     * This method actualy connects to the server
     */
    public function open() {
        // Open connection with jabber server
        $this->debug("{$this->host}, {$this->port}, {$this->socketErrorNumber}, {$this->socketErrorString}, ".$this->socketConnectionTimeout, 'Connecting...');
        $this->socket = stream_socket_client("tcp://{$this->host}:{$this->port}", $this->socketErrorNumber, $this->socketErrorString, $this->socketConnectionTimeout);
        
        $this->clientStreamStart = sprintf($this->clientStreamStart, $this->domain, self::NS_CLIENT, self::NS_STREAM, self::PROTOCOL_VERSION);
        $this->serverStreamStart = sprintf($this->serverStreamStart, self::NS_CLIENT, self::NS_STREAM, $this->domain, self::PROTOCOL_VERSION);
     
        // Create new stream. Every xmpp message is appended to root node so called stream (or more preciselly <stream::stream> adn </stream::stream>
        $this->createStreams();

        // Start tls session as it described in the RFC3920 part 7. Althrough there is an if server SHOULD always provide TLS secure connection.
        if(isset($this->features['starttls'])) {
            // send request for a negotiation
            $startTls = "<starttls xmlns='".self::NS_TLS."' />";
            fwrite($this->socket, $startTls);
            $this->debug($startTls, 'Start tls');
            
            // Get response and start negotiation
            $proceed = fread($this->socket, $this->socketReadBytes);
            stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
            $this->debug($proceed, "Proceed tls negotiation");

            // Start new stream
            $this->createStreams();
        }
    }
    
    /**
     * Close connection with server
     */
    public function close() {
        fclose($this->socket);
        $this->socket = null;
    }
    
    /**
     * Create new stream
     * 
     * When tcp connection is opened there are number of streams should be created before we may receive messages.
     * This method also parses server response for stream features and authentication mechanisms
     */
    public function createStreams() {
        // Open stream from client
        $this->debug($this->clientStreamStart, 'Client stream');
        fwrite($this->socket, "<?xml version='1.0' ?>");
        fwrite($this->socket, $this->clientStreamStart);

        // recive server stream tag
        $response  = fread($this->socket, $this->socketReadBytes);
        $response .= fread($this->socket, $this->socketReadBytes);
        
        // Check for stream error
        if(preg_match("/<stream:error>/", $response)) {
            $dom = new DOMDocument();
            $dom->loadXML(($includeStreamStart ? $this->serverStreamStart : '') . $response);
            
            throw new Zend_Jabber_Exception(Zend_Jabber_Exception::MESSAGE_STREAM_ERROR, Zend_Jabber_Exception::CODE_STREAM_ERROR);
        }
        
        $this->debug($response, 'Server stream');
        
        // Create two dom trees for loggin XMPP messages
        $this->clientStream = new DomDocument();
        $this->serverStream = new DomDocument();
        $this->clientStream->loadXML("<?xml version='1.0' ?>\n{$this->clientStreamStart}\n</stream:stream>"); 
        $this->serverStream->loadXML($response . "</stream:stream>");
        
        // Get features list
        $this->features = array();
        $this->mechanisms = array();
        if($features = $this->serverStream->getElementsByTagName('features')->item(0)) {
            foreach($features->childNodes as $feature) {
                $featureInfo['tag'] = $feature->tagName;
                $featureInfo['namespace'] = $feature->namespaceURI;
                $featureInfo['required'] = $feature->getElementsByTagName('required')->item(0) ? true : false;
                
                $this->features[$feature->tagName] = $featureInfo;
            }
            
            // Get authentication mechanisms
            foreach($features->getElementsByTagName('mechanism') as $mechanism) {
                $this->mechanisms[$mechanism->nodeValue] = $mechanism->nodeValue;
            }
        }
    }

    /**
     * Convert request object to xml string and send to server
     * 
     * @param Zend_Jabber_Stanza_Request_Abstract $request Request object
     */
    public function send(Zend_Jabber_Stanza_Request_Abstract $request) {
        if(!$this->socket) throw new Zend_Jabber_Exception(Zend_Jabber_Exception::MESSAGE_NOT_CONNECTED, Zend_Jabber_Exception::CODE_NOT_CONNECTED);

        $this->debug($request, get_class($request)); 
        $requestNode = $this->clientStream->importNode($request->toDom()->firstChild, true);
        $this->clientStream->firstChild->appendChild($requestNode);
        fwrite($this->socket,  $request->toString(). "\n");
    }
    
    /**
     * Recive xml response from server and convert it to response object
     * 
     * If no response was received for a number of seconds null response is returned
     * 
     * @see self::$socketTimeout
     * @return Zend_Jabber_Stanza
     */
    public function receive() {
        if(!$this->socket) throw new Zend_Jabber_Exception(Zend_Jabber_Exception::MESSAGE_NOT_CONNECTED, Zend_Jabber_Exception::CODE_NOT_CONNECTED);
        
        // Wait for specified amount of seconds for a response and if no response arrived return a null response
        $socket = stream_select($r = array($this->socket), $w = null, $e = null, floor($this->socketTImeout), floor(($this->socketTimeout - floor($this->socketTimeout)) * 1000000)); 
        if ($socket) {
            $dom = new DOMDocument();
            
            $response = fread($this->socket, $this->socketReadBytes) ;
			$this->debug($response, 'S:');
			
			// Some servers may close connection on athentication failure or other error
			if(!preg_match("/<\/stream:stream>$/", $response)) {
			     $response .= "</stream:stream>";
			}
			
			$dom->loadXML($this->serverStreamStart . $response);
			$stanzaNode = $dom->firstChild->firstChild;
			
			$stanzaDom = new DomDocument();
			$xml = $stanzaDom->importNode($stanzaNode, true);
			$stanzaDom->appendChild($xml);
			 
			$responseClassName = Zend_Jabber_Stanza_Abstract::getResponseClassNameByDom($stanzaDom);
			$id = Zend_Jabber_Stanza_Abstract::getStanzaIdByDom($stanzaDom);
			 
			return new $responseClassName($dom, $id);
        } 
        
        return new Zend_Jabber_Stanza_Null();
    }
    
    /**
     * Print debugin message
     * 
     * @param string $message Message text
     * @parem string $title Message title 
     */
    private function debug($message, $title = false) {
	    if(self::DEBUG) {
	        if($title) {
	            echo "\n===================================\n";
	            echo $title;
	            echo "\n===================================\n";
	        }
        
	        echo $message; 
	        echo "\n\n";
	    }
    }
}
?>